package org.dht.com.msg;

import java.io.StringReader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Set;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

import org.apache.log4j.Logger;

/**
 * The <code>Message</code> class encapsulated all message to send. This class
 * is mapped by message.xsd
 * 
 * <p>
 * Java class for Message complex type.
 * 
 * <p>
 * The following schema fragment specifies the expected content contained within
 * this class.
 * 
 * <pre>
 * <complexType name="Message">
 *   <complexContent>
 *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
 *       <sequence>
 *         <element name="type" type="{http://www.DHT-UQ.org/message}Type"/>
 *         <element name="address" type="{http://www.DHT-UQ.org/message}Address"/>
 *         <element name="params" type="{http://www.DHT-UQ.org/message}Params"/>
 *       </sequence>
 *       <attribute name="sendType" use="required">
 *         <simpleType>
 *           <restriction base="{http://www.w3.org/2001/XMLSchema}string">
 *             <enumeration value="REQUEST"/>
 *             <enumeration value="RESPONSE"/>
 *           </restriction>
 *         </simpleType>
 *       </attribute>
 *       <attribute name="sequenceNumber" use="required">
 *         <simpleType>
 *           <restriction base="{http://www.w3.org/2001/XMLSchema}long">
 *             <minInclusive value="1"/>
 *           </restriction>
 *         </simpleType>
 *       </attribute>
 *     </restriction>
 *   </complexContent>
 * </complexType>
 * </pre>
 * 
 * 
 */
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Message", propOrder = { "messageType", "address", "params" })
@XmlRootElement(name = "message")
public class MessageXML implements Message {

	/**
	 * Logger
	 */
	private static final Logger logger = Logger.getLogger(MessageXML.class);

	/**
	 * JAXB Processor
	 */
	protected static JAXBContext jaxbContext;

	/**
	 * This variable is used to determinate the next data that will be added to
	 * the message
	 */
	@XmlTransient
	private int nextData;

	/**
	 * Send type
	 */
	@XmlAttribute(required = true)
	private SendType sendType;

	/**
	 * Message type
	 */
	@XmlElement(required = true, name = "type")
	protected MessageType messageType;

	/**
	 * Address
	 */
	@XmlElement(required = true)
	protected Address address;

	/**
	 * Params
	 */
	@XmlElement(required = true)
	@XmlJavaTypeAdapter(XmlAdapterParamsMap.class)
	protected HashMap<String, String> params;

	/**
	 * Sequence number
	 */
	@XmlAttribute(required = true)
	protected long sequenceNumber;

	/**
	 * Builds a message
	 * 
	 * @param sequenceNumber
	 *            Sequence number
	 * @param sendType
	 *            Send type
	 * @param messageType
	 *            Message type
	 * @param destination
	 *            Destination name
	 * @param source
	 *            Source name
	 */
	public MessageXML(long sequenceNumber, SendType sendType,
			MessageType messageType, String destination, String source) {

		this.sequenceNumber = sequenceNumber;

		this.sendType = sendType;

		this.address = new Address(source, destination);

		this.messageType = messageType;

		this.params = new HashMap<String, String>();
	}

	/**
	 * Builds a message. The sequence number is generated from
	 * <code>SequenceGenerator</code>
	 * 
	 * @param messageType
	 *            Message type
	 * @param destination
	 *            Destination name
	 * @param source
	 *            Source name
	 */
	public MessageXML(MessageType messageType, String destination, String source) {
		this(SequenceGenerator.getSequenceNumber(), SendType.REQUEST,
				messageType, destination, source);
	}

	/**
	 * Builds message empty
	 */
	MessageXML() {

	}

	/**
	 * This method is used for adding data to the message
	 * 
	 * @param dataAdd
	 *            . The data that will be added to the message
	 */
	public void addParam(String name, String value) {
		if (nextData < messageType.getAmountParams()) {
			params.put(name, value);
		} else {
			throw new IllegalArgumentException("The message type "
					+ messageType.getName() + " must have "
					+ messageType.getAmountParams() + " parameter(s)");
		}
		nextData++;
	}

	/**
	 * This method is used for getting a specific data from the message
	 * 
	 * @param position
	 *            . The position of the data in the array
	 * @return Returns the data that is stored in the given position
	 */
	public String getParam(String name) {
		if (!params.containsKey(name)) {
			throw new IllegalArgumentException("The message type "
					+ messageType.getName() + " not contains param '" + name
					+ "'");
		} else {
			return params.get(name);
		}
	}

	/**
	 * This method is used for knowing if the message is the same source and
	 * destination node
	 * 
	 * @return Returns true if the message is the same source and destination
	 *         node
	 */
	public boolean isMessageFromMySelf() {
		return address.isMessageFromMySelf();
	}

	/**
	 * Instanciate JAXBContext
	 */
	static {
		try {
			jaxbContext = JAXBContext.newInstance(MessageXML.class);
		} catch (JAXBException e) {
			logger.error("Not should create unmarshaller", e);
		}
	}

	/**
	 * Builds a message by string. The string must strictly comply from
	 * message.xsd
	 * 
	 * @param message
	 *            Message in XML
	 * @return Message object
	 * @throws MalformedMessageException
	 *             throw when message is malformed
	 */
	public static MessageXML valueOf(String message)
			throws MalformedMessageException {
		try {

			Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();

			StringReader stringReader = new StringReader(message);

			MessageXML messageDecode = (MessageXML) unmarshaller
					.unmarshal(stringReader);

			if (messageDecode.getMessageType().getAmountParams() != messageDecode
					.getAmountParams()) {
				throw new MalformedMessageException("The message is malformed");
			}

			return messageDecode;

		} catch (JAXBException e) {
			throw new MalformedMessageException("The message is malformed", e);
		}
	}

	/**
	 * Gets the value of the type property.
	 * 
	 * @return possible object is {@link MessageType }
	 * 
	 */
	public MessageType getMessageType() {
		return messageType;
	}

	/**
	 * Sets the value of the type property.
	 * 
	 * @param value
	 *            allowed object is {@link MessageType }
	 * 
	 */
	public void setMessageType(MessageType value) {
		this.messageType = value;
	}

	/**
	 * Gets the value of the address property.
	 * 
	 * @return possible object is {@link Address }
	 * 
	 */
	public Address getAddress() {
		return address;
	}

	/**
	 * Sets the value of the address property.
	 * 
	 * @param value
	 *            allowed object is {@link Address }
	 * 
	 */
	public void setAddress(Address value) {
		this.address = value;
	}

	/**
	 * Gets the value of the params property.
	 * 
	 * @return possible object is {@link Params }
	 * 
	 */
	public HashMap<String, String> getParams() {
		return params;
	}

	/**
	 * Sets the value of the params property.
	 * 
	 * @param value
	 *            allowed object is {@link Params }
	 * 
	 */
	public void setParams(HashMap<String, String> value) {
		this.params = value;
	}

	/**
	 * Gets the value of the sequenceNumber property.
	 * 
	 */
	public long getSequenceNumber() {
		return sequenceNumber;
	}

	/**
	 * Sets the value of the sequenceNumber property.
	 * 
	 */
	public void setSequenceNumber(long value) {
		this.sequenceNumber = value;
	}

	/**
	 * Builds XML string from object message using jaxb
	 * 
	 * @return Message in XML
	 */
	public String toXML() {
		try {

			Marshaller marshaller = jaxbContext.createMarshaller();

			StringWriter stringWriter = new StringWriter();

			marshaller.marshal(this, stringWriter);

			return stringWriter.toString();

		} catch (JAXBException e) {
			logger.error("The message is malformed",
					new MalformedMessageException("The message is malformed"));
		}

		return null;
	}

	public String toString() {
		return toXML();
	}

	/**
	 * Gets amount params
	 * 
	 * @return Amount params
	 */
	public int getAmountParams() {
		return params.size();
	}

	/**
	 * Gets the message destination address
	 * 
	 * @return Message destination
	 */
	public String getMessageDestination() {
		return address.getDestination();
	}

	/**
	 * Gets the message source address
	 * 
	 * @return Message source
	 */
	public String getMessageSource() {
		return address.getSource();
	}

	/**
	 * Gets param keys
	 * 
	 * @return Param keys
	 */
	public Set<String> getParamsKey() {
		return params.keySet();
	}

	/**
	 * Gets the message type
	 * 
	 * @return Message type
	 */
	public String getType() {
		return messageType.getName();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see Message#getSendType()
	 */
	public SendType getSendType() {
		return sendType;
	}

	/**
	 * Gets the send type
	 * 
	 * @return Send type
	 */
	public void setSendType(SendType sendType) {
		this.sendType = sendType;
	}

}
